import org.molamil.commons.InvalidTypeError;
import org.molamil.commons.PropertyAlreadyExistsError;
import org.molamil.commons.StringUtil;

/**
 * Util methods to manipulate objects.
 *
 * @author Jorge Hernandez
 */
class org.molamil.commons.ObjectUtil {
	
	
	// Constants
	
	public static var ERROR_DIFF_TYPES:String = "ERROR: o1 and o2 contain the same property (\"{prop}\") with " +
												"different types: {type1} != {type2}";
	public static var ERROR_DIFF_CONSTRUCTORS:String = "ERROR: o1 and o2 contain the same property (\"{prop}\") with " +
												  	   "different types (different constructors).";
	public static var ERROR_DUPLICATED_PROP:String = "ERROR: o1 and o2 already contain the same property (\"{prop}\")";
												   
	
	// Constructor
	
	private function ObjectUtil() {}
	
	
	// Functions
	
	/**
	 * Creates a new object by copying all properties from another.
	 * 
	 * @param   o Original object, where to copy the properties from.
	 * @return  A new object containing all properties in o.
	 */
	public static function clone(o:Object):Object {
		var newObject:Object = new Object();
		for (var prop:String in o)
			newObject[prop] = o[prop];
		return newObject;
	}
	
	/**
	 * Merges two objects by creating all properties from o2 onto o1.
	 * If a property from o2 already exists in o1, the latter gets overwritten.
	 * 
	 * @param   o1 Target object where the properties are added.
	 * @param   o2 Source object, properties from this object are added to o1.
	 * 			   This object does not get modified.
	 */
	public static function merge(o1:Object, o2:Object):Void {
		for (var prop:String in o2)
			o1[prop] = o2[prop];
	}
	
	/**
	 * Merges two objects by creating all properties from o2 onto o1.
	 * If a property from o2 already exists in o1, and the types from both are different, 
	 * a <code>InvalidTypeError</code> is thrown.
	 * 
	 * @param   o1 Target object where the properties are added.
	 * @param   o2 Source object, properties from this object are added to o1.
	 * 			   This object does not get modified.
	 * @throws  PropertyAlreadyExistsError If a property from o2 was already defined in o1.
	 */
	public static function mergeTyped(o1:Object, o2:Object):Void {
		for (var prop:String in o2) {
			if (o1[prop] != null && o2[prop] != null) {
				if(typeof(o1[prop]) != typeof(o2[prop])) {
					var msg:String = StringUtil.replace(ERROR_DIFF_TYPES, "{prop}", prop);					msg = StringUtil.replace(msg, "{type1}", typeof(o1[prop]));					msg = StringUtil.replace(msg, "{type2}", typeof(o2[prop]));
					throw new InvalidTypeError(msg);
				}
				if (typeof(o2[prop]) == "object") {
					if(o1[prop].constructor != o2[prop].constructor) {
						var msg:String = StringUtil.replace(ERROR_DIFF_CONSTRUCTORS, "{prop}", prop);
						throw new InvalidTypeError(msg);
					}
				}
			}	
			o1[prop] = o2[prop];
		}
	}
	
	/**
	 * Merges two objects by creating all properties from o2 onto o1.
	 * If a property from o2 already exists in o1, a <code>PropertyAlreadyExistsError</code>
	 * is thrown.
	 * 
	 * @param   o1 Target object where the properties are added.
	 * @param   o2 Source object, properties from this object are added to o1.
	 * 			   This object does not get modified.
	 * @throws  PropertyAlreadyExistsError If a property from o2 was already defined in o1.
	 */
	public static function mergeSafe(o1:Object, o2:Object):Void {
		for (var prop:String in o2) {
			if (o1[prop] != null) {
				var msg:String = StringUtil.replace(ERROR_DUPLICATED_PROP, "{prop}", prop);
				throw new PropertyAlreadyExistsError(msg);
			}
			o1[prop] = o2[prop];
		}
	}
	
	/**
	 * Returns the value from a nested property evaluated from a String.
	 * 
	 * @param   o Target object where the property will be evaluated from.
	 * @param   nestedProperty String containing the full path to the nester property
	 *						   starting at o.
	 *						   The property can be formated using dots as in "my.nested.property"
	 *						   or forward slashes as in "my/nested/property"
	 */
	public static function getNestedProperty(o:Object, nestedProperty:String):Object {
		var s:String = StringUtil.replace(nestedProperty, "/", ".");
		var v:Object = o;
		var a:Array = s.split(".");
		for (var i:Number = 0; i < a.length; i++)
			v = v[a[i]];
		if (v == o)
			v = null;
		return v;
	}
	
	/**
	 * Traces all properties in an object as:
	 * <br /><cite>name1 = value1<br />name2 = value2<br />...<cite>
	 * 
	 * @param   o Target object where the properties will be traced from.
	 */
	public static function trace(o:Object):Void {
		for (var prop:String in o)
			trace(prop + " = " + o[prop]);
	}
	
	
}